package com.unionpay.upyzt.net;

import com.unionpay.upyzt.Upyzt;
import com.unionpay.upyzt.exception.APIConnectionException;
import com.unionpay.upyzt.exception.UpyztException;
import com.unionpay.upyzt.net.APIResource.RequestMethod;
import lombok.Getter;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Getter
public class UpyztParamRequest extends UpyztRequest {

    /**
     * 请求参数
     */
    protected Map<String, Object> params;

    /**
     * 初始化参数请求
     *
     * @param method  the HTTP method
     * @param url     the URL
     * @param params  the parameters
     * @param options the options
     * @throws UpyztException if the request cannot be initialized for any reason
     */
    public UpyztParamRequest(
            RequestMethod method,
            String url,
            Map<String, Object> params,
            RequestOptions options)
            throws UpyztException {
        try {
            this.method = method;
            this.params = (params != null) ? Collections.unmodifiableMap(params) : null;
            this.options = (options != null) ? options : RequestOptions.getDefault();
            this.url = buildURL(method, url, params);
            this.content = buildContent(method, params);
            this.authorization = buildAuthorization(this.method, this.url, this.content, this.options);
            this.headers = buildHeaders(this.authorization, this.options);
        } catch (IOException e) {
            throw new APIConnectionException(
                    String.format(
                            "请求 Upyzt (%s) 遇到 IO 异常: %s，请检查后再重试",
                            Upyzt.getApiBase(), e.getMessage()),
                    e);
        }
    }


    protected static URL buildURL(RequestMethod method, String spec, Map<String, Object> params) throws IOException {
        StringBuilder sb = new StringBuilder();

        sb.append(spec);

        if ((method != RequestMethod.POST && method != RequestMethod.PUT) && (params != null)) {
            String queryString = createQuery(params);
            if (!queryString.isEmpty()) {
                sb.append("?");
                sb.append(queryString);
            }
        }

        return new URL(sb.toString());
    }

    /**
     * @param params the parameters
     * @return queryString
     */
    protected static String createQuery(Map<String, Object> params) {
        if (params == null) {
            return "";
        }

        Map<String, String> flatParams = flattenParams(params);
        StringBuilder queryStringBuffer = new StringBuilder();
        for (Map.Entry<String, String> entry : flatParams.entrySet()) {
            if (queryStringBuffer.length() > 0) {
                queryStringBuffer.append("&");
            }
            queryStringBuffer.append(urlEncodePair(entry.getKey(),
                    entry.getValue()));
        }
        return queryStringBuffer.toString();
    }

    /**
     * @param k the key
     * @param v the value
     * @return urlEncodedString
     */
    protected static String urlEncodePair(String k, String v) {
        return String.format("%s=%s", urlEncode(k), urlEncode(v));
    }

    /**
     * @param str the string to encode
     * @return urlEncodedString
     */
    protected static String urlEncode(String str) {
        if (str == null) {
            return null;
        }

        try {
            return URLEncoder.encode(str, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError("UTF-8 is unknown");
        }
    }

    /**
     * @param params the parameters
     * @return flattenParams
     */
    protected static Map<String, String> flattenParams(Map<String, Object> params) {
        if (params == null) {
            return new HashMap<>();
        }
        Map<String, String> flatParams = new HashMap<>();
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof Map<?, ?>) {
                Map<String, Object> flatNestedMap = new HashMap<>();
                Map<?, ?> nestedMap = (Map<?, ?>) value;
                for (Map.Entry<?, ?> nestedEntry : nestedMap.entrySet()) {
                    flatNestedMap.put(
                            String.format("%s[%s]", key, nestedEntry.getKey()),
                            nestedEntry.getValue());
                }
                flatParams.putAll(flattenParams(flatNestedMap));
            } else if (value instanceof ArrayList<?>) {
                ArrayList<?> ar = (ArrayList<?>) value;
                Map<String, Object> flatNestedMap = new HashMap<>();
                int size = ar.size();
                for (int i = 0; i < size; i++) {
                    flatNestedMap.put(String.format("%s[%d]", key, i), ar.get(i));
                }
                flatParams.putAll(flattenParams(flatNestedMap));
            } else if (value == null) {
                flatParams.put(key, "");
            } else {
                flatParams.put(key, value.toString());
            }
        }
        return flatParams;
    }

    protected static HttpContent buildContent(
            RequestMethod method, Map<String, Object> params) {
        if (method != RequestMethod.POST && method != RequestMethod.PUT) {
            return null;
        }

        if (params == null) {
            return null;
        }

        return HttpContent.buildJSONContent(params);
    }


    @Override
    public void writeOutputStream(OutputStream output) throws IOException {
        output.write(this.content.getContentBytes());
    }
}
